--- title: "04-MongoDB 索引与查询优化" created: 2026-08-31 tags: - 项目筑基 --- # MongoDB 索引与查询优化 > MongoDB 系统讲解第一篇:02 篇已有索引入门(createIndex/explain 初体验),这一篇把索引**讲全讲透**——类型全家桶、复合索引的 ESR 原则、explain 输出逐字段怎么读。 ## 索引类型全家桶 ```javascript db.players.createIndex({"level": 1}) // 单字段,1 升序 -1 降序 db.players.createIndex({"server": 1, "level": -1}) // 复合索引 db.players.createIndex({"createdAt": 1}, {expireAfterSeconds: 2592000}) // TTL 索引:30 天后自动删 db.players.createIndex({"name": 1}, {unique: true}) // 唯一索引 db.players.createIndex({"email": 1}, {partialFilterExpression: {vip: true}}) // 部分索引:只索引 vip 用户 db.players.createIndex({"desc": "text"}) // 文本索引(全文搜索) db.players.createIndex({"location": "2dsphere"}) // 地理索引 ``` - **TTL 索引**是"过期自动清理"的官方方案——会话、日志类集合标配,省掉自己写定时清理任务 - **部分索引**只为符合条件的子集建索引:索引更小更快,"只查活跃用户"这类场景专用 - **唯一索引**注意:字段缺失时 null 也算重复值,多个文档缺字段会撞唯一约束(`sparse` 或部分索引可绕) ## 复合索引与 ESR 原则 复合索引 `{"a": 1, "b": 1, "c": 1}` 相当于电话簿按 a→b→c 排序。**字段顺序决定索引能不能被用上、能用多少**: **ESR(Equality - Sort - Range)**: 1. **E**:等值条件的字段放最前 2. **S**:排序字段放其次 3. **R**:范围查询的字段放最后 例:查询 `{"server": "X"}` 按 `level` 排序、再筛 `{"level": {"$gt": 20}}`——索引应该是 `{"server": 1, "level": 1}`(server 是 E,level 先承担排序再承担范围,两者重合)。 > 💡 为什么 R 要放最后:范围字段之后的索引字段就"断档"了——按 a 查、b 范围查、c 精确查的索引 `{"a":1,"b":1,"c":1}`,b 的范围性让 c 无法走索引前缀,只能过滤。**ESR 记忆钩子:先等值、再排序、最后范围**。 **前缀规则**:`{a,b,c}` 的索引可支撑 `{a}`、`{a,b}` 的查询,但撑不起 `{b}`——所以"每个查询建一个索引"是错的,按查询族设计复合索引。 ## explain:三种模式与关键字段 ```javascript db.players.find({"server": "东方明珠", "level": {"$gt": 20}}) .sort({"level": -1}) .explain("executionStats") // 最常用:真的执行并给统计 ``` 逐字段读法(executionStats 层): | 字段 | 含义 | 期望 | | --- | --- | --- | | `nReturned` | 返回条数 | — | | `totalKeysExamined` | 走索引扫了多少条 | 与 nReturned 接近 | | `totalDocsExamined` | 回表读了多少文档 | **与 nReturned 接近=索引高效** | | `executionTimeMillis` | 耗时 | — | 三个红旗:`COLLSCAN`(全集合扫描,该建索引了);`totalDocsExamined >> nReturned`(索引选得不准或 ESR 顺序不对);`SORT stage` 内存排序(排序没被索引吃掉,数据量大时可能报内存超限错误)。 ## 索引的代价与设计清单 索引不是免费的: - **写放大**:每次 insert/update 都要同步维护所有相关索引 - **内存占用**:索引要常驻内存(WiredTiger 缓存)才快,被挤出去就是灾难 - **数量红线**:单集合索引别无脑堆,书里的常见建议是生产集合控制在个位数到十来个,冗余索引(被复合索引前缀覆盖的单字段索引)定期清理 我的设计清单(书里经验的汇总): 1. 先看 explain 确认真的是索引问题,再建索引 2. 复合索引按 ESR 排,覆盖常用查询族 3. 排序字段尽量进索引,避免内存 SORT 4. 分页别用大 skip(翻得越深越慢),用 `{"_id": {"$gt": 上一页最后一个}}` 基于游标翻页 5. TTL/部分索引按需上,用它们省内存 --- ⬅️ [[03-MongoDB 理解|03-MongoDB 理解]] 🏠 [[00-数据库|00-数据库]] ➡️ [[05-MongoDB 副本集与分片|05-MongoDB 副本集与分片]]